package com.gitee.wsl.mathematics.geometry.d2.contour

import com.gitee.wsl.mathematics.geometry.api.ShapeTopology
import com.gitee.wsl.mathematics.geometry.api.Winding
import com.gitee.wsl.mathematics.geometry.d2.api.ShapeProvider
import com.gitee.wsl.mathematics.geometry.d2.contour.region.Region2
import com.gitee.wsl.mathematics.geometry.d2.rect.Rectangle
import com.gitee.wsl.mathematics.geometry.d2.rect.ext.bounds
import com.gitee.wsl.mathematics.geometry.d2.triangle.Triangle
import com.gitee.wsl.mathematics.matrix.mat4.Mat4f
import com.gitee.wsl.mathematics.vector.ext.squaredDistanceTo
import com.gitee.wsl.mathematics.vector.vec2.Vec2f
import com.gitee.wsl.reflect.resettableLazy
import kotlinx.serialization.Transient


/**
 * A simple interface for managing a [List] of [ShapeContour].
 */
class ContourShape(val contours: List<ShapeContour>): ShapeProvider {
    companion object {
        /**
         * An empty [ContourShape] object.
         *
         * It is advised to use this instance whenever an empty shape is needed.
         */
        val EMPTY: ContourShape = ContourShape(emptyList())

        /** Creates a [ContourShape] from combining a [List] of Shapes */
        fun compound(shapes: List<ContourShape>) = ContourShape(shapes.flatMap { it.contours })
    }

    /** Returns [ContourShape] bounding box. */
    @Transient
    private val boundsDelegate = resettableLazy {
        if (empty) {
            Rectangle(0.0, 0.0, 0.0, 0.0)
        } else {
            val result: List<Rectangle> = contours.mapNotNull {
                if (it.empty) {
                    null
                } else {
                    it.bounds
                }
            }
            result.bounds
        }
    }

    val bounds by boundsDelegate

    /** Indicates the [ContourShape] topology. */
    @Transient
    val topology = when {
        contours.isEmpty() -> ShapeTopology.OPEN
        contours.all { it.closed } -> ShapeTopology.CLOSED
        contours.all { !it.closed } -> ShapeTopology.OPEN
        else -> ShapeTopology.MIXED
    }


    @Transient
            /** Lists all [ShapeContour]s with an [open topology][ShapeTopology.OPEN]. */
    val openContours: List<ShapeContour> =
        when (topology) {
            ShapeTopology.OPEN -> contours
            ShapeTopology.CLOSED -> emptyList()
            ShapeTopology.MIXED -> contours.filter { !it.closed }
        }


    /** Lists all [ShapeContour]s with a [closed topology][ShapeTopology.CLOSED]. */
    @Transient
    val closedContours: List<ShapeContour> =
        when (topology) {
            ShapeTopology.OPEN -> emptyList()
            ShapeTopology.CLOSED -> contours
            ShapeTopology.MIXED -> contours.filter { it.closed }
        }

    /** Returns true if [ContourShape] contains no [ShapeContour]s. */
    val empty get() = this === EMPTY || contours.isEmpty()


    /**
     * Projects a point on the contours of a [ContourShape]
     * @param point The point to project.
     * @return a projected point that lies on the [ShapeContour].
     */
    fun nearest(point: Vec2f): ContourPoint {
        require(!empty) {
            """cannot perform nearest point query on empty shape"""
        }
        return contours.map { it.nearest(point) }.minByOrNull { it.position.squaredDistanceTo(point) }
            ?: error("no nearest segment")
    }

    /**
     * Returns true if [ContourShape] consists solely of [ShapeContour]s,
     * where each [Segment2D] is a [line segment][SegmentType.LINEAR].
     */
    val linear get() = contours.all { it.segments.all { segment -> segment.linear } }
    fun polygon(distanceTolerance: Double = 0.5) =
        if (empty) {
            EMPTY
        } else {
            ContourShape(contours.map { it.sampleLinear(distanceTolerance) })
        }

    @Transient
    private val triangulationDelegate = resettableLazy {
        Triangle(Vec2f.ZERO,Vec2f.ZERO,Vec2f.ZERO)
        /*triangulate(this).windowed(3, 3).map {
            Triangle(it[0], it[1], it[2])
        }*/
    }


    /** Triangulates [ContourShape] into a [List] of [Triangle]s. */
    val triangulation by triangulationDelegate

    @Transient
    private val areaDelegate = resettableLazy {
        10f
        //triangulation.sumOf { it.area }
    }

    /** Calculates approximate area for this shape (through triangulation). */
    val area by areaDelegate

    @Transient
    private val region2Delegate = resettableLazy {
        Region2(contours.map { it.ring2 })
    }

    internal val region2 by region2Delegate

    @Transient
    private val path2Delegate = resettableLazy {
        contours.map { it.path2 }
    }

    internal val path2 by path2Delegate

    fun resetCache() {
        boundsDelegate.reset()
        triangulationDelegate.reset()
        areaDelegate.reset()
        region2Delegate.reset()
        path2Delegate.reset()
        contours.forEach { it.resetCache() }
    }



    /** The outline of the shape. */
    val outline get() = contours[0]

    /**
     * The indexed hole of the shape.
     * @param index
     */
    fun hole(index: Int) = contours[index + 1]

    /**
     * Applies a linear transformation to the [ContourShape].
     *
     * @param transform A [Matrix44] that represents the transform.
     * @return A transformed [ContourShape] instance
     */
    fun transform(transform: Mat4f) = when {
        empty -> EMPTY
        transform === Mat4f.IDENTITY -> this
        else -> ContourShape(contours.map { it.transform(transform) })
    }

    /** Applies a map to the shape. Maps every contour. */
    fun map(mapper: (ShapeContour) -> ShapeContour) = ContourShape(contours.map { mapper(it) })

    /**
     * Checks whether the [ContourShape] is org.openrndr.shape.compound or not.
     *
     * Returns true when there's multiple [ShapeContour]s with a [clockwise winding][Winding.CLOCKWISE]
     * because the function only counts non-hole ShapeContours (which are clockwise).
     */
    val compound: Boolean
        get() {
            return if (contours.isEmpty()) {
                false
            } else {
                contours.count { it.winding == Winding.CLOCKWISE } > 1
            }
        }

    /** Splits an org.openrndr.shape.compound shape into separate shapes. */
    fun splitCompounds(winding: Winding = Winding.CLOCKWISE): List<ContourShape> {
        return if (contours.isEmpty()) {
            emptyList()
        } else {
            val (cw, ccw) = closedContours.partition { it.winding == winding }
            val candidates = cw.map { outer ->
                val cs = ccw.filter { it.bounds.intersects(outer.bounds) }
                listOf(outer) + cs
            }
            (candidates + openContours.map { listOf(it) }).map { ContourShape(it) }
        }
    }

     override val shape: ContourShape
        get() = this

    override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (other !is ContourShape) return false

        if (contours != other.contours) return false

        return true
    }

    override fun hashCode(): Int {
        return contours.hashCode()
    }

    override fun toString(): String {
        return "Shape(org.openrndr.shape.contours=$contours, topology=$topology)"
    }
}

/** Converts a [List] of [ContourShape] items into a single org.openrndr.shape.compound [ContourShape]. */
@Transient
val List<ContourShape>.compound
    get() = ContourShape.compound(this)

